package com.itstyle.seckill.common.aop;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.itstyle.seckill.common.exception.RrException;
import com.itstyle.seckill.common.utils.IPUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.Configuration;

import com.google.common.util.concurrent.RateLimiter;
import java.lang.reflect.Method;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**
 * 限流 AOP
 * 创建者	张志朋
 * 创建时间	2015年6月3日
 */



@Aspect
@Configuration
public class LimitAspect {

	/**
	 * LoadingCache就是一个增强版的map，存放的是key，value
	 *
	 *
	 */
	//根据IP分不同的令牌桶, 每天自动清理缓存
	private static LoadingCache<String, RateLimiter> caches = CacheBuilder.newBuilder()
			.maximumSize(1000) //存储的值的最大个数(当添加的数量大于这个值的时候，会自动删除前面添加的元素)
			.expireAfterWrite(1, TimeUnit.DAYS) //过期时间（在指定时间没有写入，就全部过期）
			.build(new CacheLoader<String, RateLimiter>() {
				@Override
				public RateLimiter load(String key) {
					System.out.println("加载新的ip："+key);
					// 新的IP初始化 每秒只发出5个令牌
					return RateLimiter.create(50);//每秒请求数
				}
			});



	//Service层切点  限流
	@Pointcut("@annotation(com.itstyle.seckill.common.aop.ServiceLimit)")
	public void ServiceAspect() {

	}


	public static void main(String[] args) throws Exception {


		for (int i=0;i<10;i++){
			Thread.sleep(100);
			login(String.valueOf(4));
		}

	}

	public static void login(String i) throws ExecutionException {
		/**
		 * caches在get元素的时候，如果不存在，load方法就会自动加载该对象，返回一个新的RateLimiter
		 * 每个新的RateLimiter包含50个令牌。
		 * 基于"令牌桶"实现限流。
		 * 令牌桶：在桶里面，初始化一定数量的令牌，然后，按照固定时间内，只能发放这么多令牌。
		 * 这样保证了，控制单位时间内的访问量。
		 *
		 * 想对于"漏斗桶"，不仅可以保证速率，还可以防止突增的流量。
		 *
		 *
		 * 漏桶：
		 *
		 * 把请求比作是水，水来了都先放进桶里，并以限定的速度出水，当水来得过猛而出水不够快时就会导致水直接溢出，即拒绝服务。
		 *
		 * ps:把队列当做桶，请求添加到队列的速度不用控制，但按指定速率从队列取请求。
		 * 对于突然新增很多请求，处理速度跟不上，就会导致队列满，从而拒绝继续插入队列，即拒绝服务。
		 *
		 * 令牌桶：
		 *
		 令牌桶算法的原理是系统以恒定的速率产生令牌，然后把令牌放到令牌桶中，令牌桶有一个容量，当令牌桶满了的时候，再向其中放令牌，那么多余的令牌会被丢弃；
		 当想要处理一个请求的时候，需要从令牌桶中取出一个令牌，如果此时令牌桶中没有令牌，那么则拒绝该请求
		 *

		 *
		 *
		 * 场景：速率为300/min，即每分钟处理300个请求
		 * "漏桶"是流入速度不固定，流出固定。--这样突然暴增的请求，即使桶没满，也处理不过来，因为处理请求的速度是固定的。
		 * 		1.当前10s就来了290个请求，这个时候，虽然桶没满，但是因为处理的速度是固定的，所以还是会存在大量请求没被处理。
		 * "令牌桶"是生产"令牌"速度固定，取"令牌"速度不固定。--当"令牌桶"内有"令牌"，假设速度限制为30/s，
		 * 		2.当前10s就来了290个请求，这个时候桶里面如果已经有300个令牌，那么可以在前10s将290个请求都处理了。
		 *
		 * 		3.无论是"漏桶"还是"令牌桶"都是在请求超出限定后拒绝服务，但是"令牌桶"在没超出请求量，但是是突然暴增请求的场景处理得更好。
		 * 			当1min中有500个请求过来，那么两种桶都会拒绝请求。
		 */

		RateLimiter rateLimiter = caches.get(i);
		Boolean flag = rateLimiter.tryAcquire();
		if(flag){
			System.out.println(i+"访问");
		}else{
			throw new RrException("小同志，你访问的太频繁了");
		}
	}



	@Around("ServiceAspect()")
	public  Object around(ProceedingJoinPoint joinPoint) {
		MethodSignature signature = (MethodSignature) joinPoint.getSignature();
		Method method = signature.getMethod();
		ServiceLimit limitAnnotation = method.getAnnotation(ServiceLimit.class);
		ServiceLimit.LimitType limitType = limitAnnotation.limitType();
		String key = limitAnnotation.key();
		Object obj;
		try {
			if(limitType.equals(ServiceLimit.LimitType.IP)){
				key = IPUtils.getIpAddr();
			}
			RateLimiter rateLimiter = caches.get(key);
			Boolean flag = rateLimiter.tryAcquire();
			if(flag){
				obj = joinPoint.proceed();
			}else{
				throw new RrException("小同志，你访问的太频繁了");
			}
		} catch (Throwable e) {
			throw new RrException("小同志，你访问的太频繁了");
		}
		return obj;
	}
}
